interface IHandle {
	destroy(this: IHandle): void;
}

const has_formdata = typeof FormData !== 'undefined';

function noop() { }

interface IRequestOptions {
	blockMainThread?: boolean;
	auth?: string;
	cacheBust?: boolean;
	data?: string | FormData;
	headers?: { [name: string]: string; };
	method?: string;
	password?: string;
	query?: { [key: string]: string; };
	timeout?: number;
	user?: string;
}

function generateRequestUrl(url: string, options: IRequestOptions) {
	const usp = new URLSearchParams();
	const query_param = options.query || {};
	for (const k in query_param) {
		if (query_param.hasOwnProperty(k)) {
			usp.append(k, query_param[k]);
		}
	}
	let query = usp.toString();
	// let query = obj2query(options.query || {});

	if (options.cacheBust) {
		const cacheBust = String(Date.now());
		query += query ? '&' + cacheBust : cacheBust;
	}

	const separator = url.indexOf('?') > -1 ? '&' : '?';
	return query ? url + separator + query : url;
}

export interface IResponse {
	responseText: string;
	responseType: string;
	responseXML: Document | null;
	status: number;
	statusText: string;
	getResponseHeader(name: string): string;
	getAllResponseHeaders(): string;
}

function createHandle(destructor: () => void): IHandle {
	return {
		destroy() {
			this.destroy = noop;
			destructor.call(this);
		}
	};
}

function createTimer(callback: () => void, delay?: number): IHandle {
	const timerId = setTimeout(callback, delay);

	return createHandle(() => {
		clearTimeout(timerId);
	});
}

// export interface ResponsePromise<T> extends Task<Response<T>> {}

function xhr(url: string, options: IRequestOptions = {}) {
	const request = new XMLHttpRequest();
	const requestUrl = generateRequestUrl(url, options);
	const response = {
		responseText: '',
		responseType: '',
		responseXML: null,
		status: 0,
		statusText: '',
		getResponseHeader(name: string) {
			return request.getResponseHeader(name);
		},
		getAllResponseHeaders() {
			return request.getAllResponseHeaders();
		}
	} as IResponse;
	let isAborted = false;

	function abort() {
		isAborted = true;
		if (request) {
			request.abort();
			request.onreadystatechange = noop;
		}
	}

	return new Promise<IResponse>((resolve, reject) => {
		if (!options.method) {
			options.method = 'GET';
		}

		if ((!options.user || !options.password) && options.auth) {
			const auth = options.auth.split(':');
			options.user = decodeURIComponent(auth[0]);
			options.password = decodeURIComponent(auth[1]);
		}
		request.open(options.method, requestUrl, !options.blockMainThread, options.user, options.password);
		request.withCredentials = true;

		let timeoutHandle: IHandle;
		request.onreadystatechange = () => {
			if (!isAborted && request.readyState === 4) {
				request.onreadystatechange = noop;
				if (timeoutHandle) {
					timeoutHandle.destroy();
				}

				response.responseText = request.responseText;
				response.responseType = request.responseType;
				response.responseXML = request.responseXML;
				response.status = request.status;
				response.statusText = request.statusText;
				if (response.status > 0 && response.status < 400) {
					resolve(response);
				} else {
					reject(new Error(response.responseText));
				}
			}
		};

		if (options.timeout !== undefined && options.timeout > 0 && options.timeout !== Infinity) {
			timeoutHandle = createTimer(() => {
				// Reject first, since aborting will also fire onreadystatechange which would reject with a
				// less specific error.  (This is also why we set up our own timeout rather than using
				// native timeout and ontimeout, because that aborts and fires onreadystatechange before ontimeout.)
				response.statusText = 'The XMLHttpRequest request timed out.';
				reject(new Error(response.statusText));
				abort();
			}, options.timeout);
		}

		const headers = options.headers;
		if (headers) {
			let hasContentTypeHeader = false;
			Object.keys(headers).forEach((header) => {
				if (header.toLowerCase() === 'content-type') {
					hasContentTypeHeader = true;
				}

				request.setRequestHeader(header, headers[header]);
			});

			if (!headers || !('X-Requested-With' in headers)) {
				request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
			}

			if (!hasContentTypeHeader && has_formdata && options.data instanceof FormData) {
				// Assume that most forms do not contain large binary files. If that is not the case,
				// then "multipart/form-data" should be manually specified as the "Content-Type" header.
				request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
			}
		}

		// if (options.responseType === 'xml' && request.overrideMimeType) {
		// 	// This forces the XHR to parse the response as XML regardless of the MIME-type returned by the server
		// 	request.overrideMimeType('text/xml');
		// }

		request.send(options.method === 'POST' ? options.data : null);
	});
}

/**
 * get请求
 * @function
 * @access public
 * @param  {string} url 请求地址
 * @param  {object} [data] 请求参数
 * @param  {number} [timeout] 超时设置
 * @return {object} 返回的数据，即请求返回的resultJson数据
 * @example
 * let res = await request.get(url);
 */
export function get(url: string, data?: {
	[key: string]: string;
}, timeout?: number) {
	return xhr(url, {
		cacheBust: true,
		headers: {
			'Content-Type': 'text/html; charset=utf-8'
		},
		method: 'GET',
		query: data,
		timeout
	});
}

/**
 * post
 * @function
 * @access public
 * @param  {string} url 请求地址
 * @param  {object} [data] 请求参数
 * @param  {number} [timeout] 超时设置
 * @return {object} 返回的数据，即请求返回的resultJson数据
 * @example
 * let res = await request.post(url, { abc:'001' });
 */
export function post(url: string, data?: string | FormData, query?: {
	[key: string]: string;
}, timeout?: number) {
	return xhr(url, {
		data,
		headers: {
			'Content-Type': 'text/json; charset=utf-8'
		},
		method: 'POST',
		query,
		timeout
	});
}
